import { ComponentFixture, TestBed, waitForAsync, fakeAsync, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, TemplateRef } from '@angular/core';
import { PopoverModule } from './popover.module';
import { ButtonModule } from '../button/button.module';
import { TooltipModule } from '../tooltip/tooltip.module';
import { InputBoolean } from '../core/api';

const POSITIONS = [
  'top',
  'topLeft',
  'topRight',
  'right',
  'rightTop',
  'rightBottom',
];

@Component({
  template: `
    <button
      lv-button
      lv-popover
      [lvPopoverHeader]="lvHeader"
      [lvPopoverContent]="lvContent"
      [lvPopoverFooter]="lvFooter"
      [lvPopoverTheme]="lvTheme"
      [lvPopoverTrigger]="lvTrigger"
      [lvPopoverPosition]="lvPosition"
      [lvPopoverClosable]="lvClosable"
      [lvPopoverBackdrop]="lvBackdrop"
      [lvPopoverMaskTransparent]="lvMaskTransparent"
      [lvPopoverBlockPageScroll]="lvBlockPageScroll"
      [lvPopoverScrollToClose]="lvScrollToClose"
      [lvPopoverOuterClosable]="lvOuterClosable"
      [lvPopoverClassName]="lvClassName"
      [(lvPopoverVisible)]="lvVisible"
      [lvPopoverShowArrow]="lvShowArrow"
      [lvPopoverArrowPointAtCenter]="lvArrowPointAtCenter"
      [lvPopoverBeforeOpen]="lvBeforeOpen"
      [lvPopoverBeforeClose]="lvBeforeClose"
      (lvPopoverExternalTrigger)="externalClose()"
      (click)="toggle()"
    >
      Trigger
    </button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestPopoverComponent {
  @Input() lvHeader: string | TemplateRef<void> = 'Title';
  @Input() lvContent: string | TemplateRef<void> = 'This is content...';
  @Input() lvFooter: string | TemplateRef<void> = 'Footer';
  @Input() lvTheme: 'dark' | 'light' = 'light';
  @Input() lvTrigger: 'hover' | 'foucs' | 'click' | 'customize' = 'hover';
  @Input() lvPosition:
    | 'top'
    | 'topLeft'
    | 'topRight'
    | 'right'
    | 'rightTop'
    | 'rightBottom'
    | 'bottom'
    | 'bottomLeft'
    | 'bottomRight'
    | 'left'
    | 'leftTop'
    | 'leftBottom' = 'top';
  @Input() @InputBoolean() lvClosable: boolean = false;
  @Input() @InputBoolean() lvBackdrop: boolean = false;
  @Input() @InputBoolean() lvMaskTransparent: boolean = false;
  @Input() @InputBoolean() lvBlockPageScroll: boolean = true;
  @Input() lvScrollToClose: boolean | { exclusions: HTMLElement[] } = true;
  @Input() @InputBoolean() lvOuterClosable: boolean | { exclusions: HTMLElement[]; includes?: string[] } = true;
  @Input() @InputBoolean() lvShowArrow: boolean = true;
  @Input() @InputBoolean() lvArrowPointAtCenter: boolean = false;
  @Input() lvClassName: string = '';
  @Input() lvPopoverTrigger: 'hover' | 'click' = 'hover';
  @Input() lvBeforeOpen: () => (false | void | {}) | Promise<false | void | {}>;
  @Input() lvBeforeClose: () => (false | void | {}) | Promise<false | void | {}>;

  lvVisible: boolean = false;

  toggle(): void {
    if (this.lvTrigger === 'customize') {
      this.lvVisible = !this.lvVisible;
    }
  }

  beforeOpen = (): Promise<boolean> => new Promise(resolve => resolve(false));

  beforeClose = (): Promise<boolean> => new Promise(resolve => resolve(false));

  externalClose(): void {}
}

describe('PopoverDirective', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [ButtonModule, PopoverModule, NoopAnimationsModule],
      declarations: [TestPopoverComponent],
      teardown: { destroyAfterEach: true },
    }).compileComponents;
  }));

  let component: TestPopoverComponent;
  let fixture: ComponentFixture<TestPopoverComponent>;
  let cdr: ChangeDetectorRef;

  const getPopover = () => document.querySelector('.lv-popover');

  const detectChanges = (isCdr: boolean = false, delay = 500) => {
    tick(delay);
    if (isCdr) {
      cdr.detectChanges();
    } else {
      fixture.detectChanges();
    }
  };

  describe('render', () => {
    const renderComponent = params => {
      for (const [key, value] of Object.entries(params)) {
        component[key] = value;
      }
      cdr.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('mouseenter'));
      tick(500);
      detectChanges();
      expect(getPopover()).toMatchSnapshot();
      button.dispatchEvent(new Event('mouseleave'));
      tick(500);
      detectChanges();
    };

    beforeEach(() => {
      fixture = TestBed.createComponent(TestPopoverComponent);
      component = fixture.componentInstance;
      cdr = fixture.debugElement.injector.get(ChangeDetectorRef);
      fixture.detectChanges();
    });

    it('with default', fakeAsync(() => {
      renderComponent({});
    }));

    it('with lvPosition', fakeAsync(() => {
      POSITIONS.slice(1).forEach(key => {
        renderComponent({ lvPosition: key });
      });
    }));

    it('with lvBackdrop', fakeAsync(() => {
      renderComponent({ lvBackdrop: true });
      renderComponent({ lvBackdrop: true, lvMaskTransParent: true });
    }));

    it('without lvShowArrow', fakeAsync(() => {
      renderComponent({ lvShowArrow: false });
    }));

    it('with lvTheme', fakeAsync(() => {
      renderComponent({ lvTheme: 'dark' });
    }));

    it('with lvClosable', fakeAsync(() => {
      renderComponent({ lvCloseable: true });
    }));

    it('with lvArrowPointAtCenter', fakeAsync(() => {
      renderComponent({ lvPosition: 'topLeft', lvArrowPointAtCenter: true });
    }));
  });

  describe('works correctly', () => {
    beforeEach(() => {
      fixture = TestBed.createComponent(TestPopoverComponent);
      component = fixture.componentInstance;
      cdr = fixture.debugElement.injector.get(ChangeDetectorRef);
    });

    it('with lvTrigger click', fakeAsync(() => {
      component.lvTrigger = 'click';
      fixture.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('click'));
      detectChanges();
      expect(getPopover()).toBeTruthy();
      button.dispatchEvent(new Event('mouseleave'));
      detectChanges();
      expect(getPopover()).toBeTruthy();
      document.body.dispatchEvent(new Event('click'));
      detectChanges();
      expect(getPopover()).toBeTruthy();
    }));

    it('with lvTrigger is customize', fakeAsync(() => {
      component.lvTrigger = 'customize';
      fixture.detectChanges();

      document.dispatchEvent(new Event('mouseover'));
      detectChanges();

      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('click'));
      detectChanges();
      detectChanges();
      expect(getPopover()).toBeTruthy();

      button.dispatchEvent(new Event('click'));
      detectChanges();
      detectChanges();
      detectChanges();
      expect(getPopover()).toBeNull();
    }));

    it('with lvClosable', fakeAsync(() => {
      component.lvClosable = true;
      fixture.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('mouseenter'));
      detectChanges();
      expect(getPopover()).toBeTruthy();
      const closeButton = document.querySelector('.lv-popover-close');
      closeButton.dispatchEvent(new Event('click'));
      detectChanges();
      detectChanges();
      expect(getPopover()).toBeNull();
    }));

    it('with lvBeforeOpen', fakeAsync(() => {
      jest.spyOn(component, 'beforeOpen');
      component.lvBeforeOpen = component.beforeOpen;
      fixture.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('mouseenter'));
      detectChanges();
      expect(getPopover()).toBeNull();
      expect(component.beforeOpen).toBeCalledTimes(1);
    }));

    it('with lvBeforeClose', fakeAsync(() => {
      jest.spyOn(component, 'beforeClose');
      component.lvBeforeClose = component.beforeClose;
      component.lvClosable = true;
      fixture.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('mouseenter'));
      detectChanges();
      expect(getPopover()).toBeTruthy();

      const closeButton = document.querySelector('.lv-popover-close');
      closeButton.dispatchEvent(new Event('click'));
      detectChanges();
      expect(getPopover()).toBeTruthy();
      expect(component.beforeClose).toBeCalledTimes(1);
    }));

    it('with lvExternalTrigger', fakeAsync(() => {
      jest.spyOn(component, 'externalClose');
      component.lvClosable = true;
      component.lvTrigger = 'click';
      component.lvBackdrop = true;
      fixture.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('click'));
      detectChanges();
      expect(getPopover()).toBeTruthy();

      const closeButton = document.querySelector('.lv-popover-close');
      closeButton.dispatchEvent(new Event('click'));
      detectChanges();
      detectChanges();
      expect(getPopover()).toBeNull();
      expect(component.externalClose).toBeCalledTimes(0);

      button.dispatchEvent(new Event('click'));
      detectChanges();
      detectChanges();
      expect(getPopover()).toBeTruthy();

      const backdrop = document.querySelector('.cdk-overlay-backdrop');
      backdrop.dispatchEvent(new Event('click'));
      detectChanges();
      detectChanges();
      expect(getPopover()).toBeNull();
      expect(component.externalClose).toBeCalledTimes(1);
    }));

    it('with lvClassName', fakeAsync(() => {
      const className = 'aui-custom-popover';
      component.lvClassName = className;
      fixture.detectChanges();
      const button = fixture.nativeElement.querySelector('.lv-button');
      button.dispatchEvent(new Event('mouseenter'));
      detectChanges();
      expect(getPopover().className).toContain(className);
    }));
  });
});

@Component({
  template: `
    <div style="width: 10rem; height: 10rem">
      <button
        lv-popover
        lvPopoverHeader="Title"
        [lvPopoverContent]="content"
        lvPopoverFooter="Footer"
        lvPopoverTrigger="click"
        [lvPopoverClosable]="true"
        lvPopoverTheme="light"
        lv-button
      >
        Click me
      </button>
      <ng-template #content>
        <div style="width: 2rem">
          <span lv-tooltip="The component has its own close button.">The component has its own close button.</span>
        </div>
      </ng-template>
    </div>
  `,
})
class TestPopoverOverflowComponent {}

describe('PopoverOverflow', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [PopoverModule, ButtonModule, TooltipModule, NoopAnimationsModule],
      declarations: [TestPopoverOverflowComponent],
      teardown: { destroyAfterEach: false },
    }).compileComponents;
  }));
  describe('basic', () => {
    let fixture: ComponentFixture<TestPopoverOverflowComponent>;
    const getOverflow = () => document.querySelector('.lv-tooltip-container');
    const getPopover = () => document.querySelector('.lv-popover');
    const getOverflowOrigin = () => getPopover().querySelector('span');
    beforeEach(() => {
      fixture = TestBed.createComponent(TestPopoverOverflowComponent);
    });
    it('with lvPopoverTrigger is click', fakeAsync(() => {
      fixture.detectChanges();
      tick(500);
      fixture.nativeElement.querySelector('.lv-button').dispatchEvent(new Event('click'));
      tick(500);
      fixture.detectChanges();
      expect(getPopover()).toBeTruthy();
      expect(getOverflowOrigin()).toBeTruthy();
      getOverflowOrigin().dispatchEvent(new Event('mouseenter'));
      tick(500);
      fixture.detectChanges();
      expect(getOverflow()).toBeTruthy();
      getOverflow().dispatchEvent(new Event('mouseenter'));
      tick(500);
      fixture.detectChanges();
      getOverflow().dispatchEvent(new Event('mouseover'));
      tick(500);
      fixture.detectChanges();
      expect(getOverflow()).toBeTruthy();
      expect(getPopover()).toBeTruthy();
      document.dispatchEvent(new Event('mouseover'));
      tick(1000);
      fixture.detectChanges();
      getOverflowOrigin().dispatchEvent(new Event('mouseleave'));
      tick(1000);
      fixture.detectChanges();
      tick(1000);
      fixture.detectChanges();
      tick(1000);
      fixture.detectChanges();
      expect(getOverflow()).toBeNull();
      expect(getPopover()).toBeTruthy();
    }));
  });
});
